module nazo.http;
/**
 * HttpのCgi向けAPI
 * History:
 *          0.0.0.5 色々改変
 *          0.0.0.4 basePathの追加
 *          0.0.0.3 高速化
 *          0.0.0.2 鯖上以外でインポートした時のバグ修正
 *          0.0.0.1 バージョン付け開始
 * Authors: Nazo
 * Version: 0.0.0.5
 * License: Public Domain
 */
import nazo.string, std.stdio, nazo.zlib, std.conv, std.regexp, nazo.savelist, nazo.stdio, nazo.env, nazo.log, std.uri, std.cstream, core.thread, std.conv, std.socket, std.contracts;

/**
 * HTTPを扱うクラス
 * Bugs: multipart/form-dataはテストしてない。
 *       gzipに未対応
 *       port80以外やhttps使用時は未テスト(問題が起こる可能性が高い)
 */
class Http{
  public{
    enum methods{
      GET,
      POST,
      HEAD,
      OPTIONS,
      PUT,
      DELETE,
      TRACE,
      UNKNOWN,
    }
    private static string scriptname;
    private static string bin;
    static string getScriptName(){
      return scriptname;
    }
    private static bool isnph;
    static bool isNph(){
      return isnph;
    }
    static methods method;
    ///セキュリティーエラーを吐く
    static void securityError(){
      HttpResponse.write("Security Error!",403);
    }
    ///そのAcceptには対応してませんというエラーを吐く
    static void acceptError(){
      HttpResponse.write("そのAcceptには対応してませんよん",406);
    }
    ///リクエストがGETとPOST以外ならfalseを返す。(通常HEAD以外はありえないと思われ(PUTやDELETE、TARCEはApacheが防いでくれるとおも)。HEADは攻撃に役立つので防いどく)
    static bool isAllowMethod(){
      return cast(bool)(method==methods.POST||method==methods.GET);
    }
    ///GETならtrueを返す
    static bool isGetMethod(){
      return cast(bool)(method==methods.GET);
    }
    ///POSTならtrueを返す
    static bool isPostMethod(){
      return cast(bool)(method==methods.POST);
    }    
    ///POST向けのこれ自身から呼び出されたかどうかをチェック
    static bool isValidReferer(){
      string referer=Env.TGet!("HTTP_REFERER")();
      if(referer==""){
        return false;
      }
      //pathinfoを考慮
      string schema=referer.getBefore(':');
      if(schema!="http"&&schema!="https")return false;
      referer=referer[schema.length+3..$];
      string refhost=getBefore(referer,'/');
      string tmp=getBefore(referer[refhost.length..$],'?');
      refhost=getBefore(refhost,':');//XXX 未テスト
      if(refhost==Env.TGet!("SERVER_NAME")())
        if(startWith(tmp,getScriptName))
          return true;
      return false;
    }
    ///Content-Typeのリストからどれを使うかを選ぶ
    ///Reterns: 何返していいか分からなかったら空文字列を返します。
    static string allowAccept(string[] lst){
      if(accept=="")return "";
      string[] acceptArray;
      getBefores(accept,',',delegate void(string i){acceptArray~=strip(getBefore(i,';'));});
      foreach(string type;lst){
        foreach(string i;acceptArray){
          if(i==type){
            return type;
          }
        }
      }
      return "";
    }
    ///Accept-Langageを返す
    ///Bugs: リストからどれを選ぶか方式にした方がいいが面倒臭い。
    static string acceptLanguage(){
      return acceptlanguage;
    }
/+    ///キャラクタリストからどれを使うかを選ぶ
    ///Bugs: Not Impl
    string acceptCharset(string[] lst){
      return this.Env.TGet!("HTTP_ACCEPT_CHARSET")();
    }
    ///エンコードのリストからどれを使うかを選ぶ
    ///Returns: lstの中に含まれていた場合はそれを、そうでない場合は""を返す。
    ///Bugs: Not Impl
    string acceptEncoding(string[] lst){
      return this.Env.TGet!("HTTP_ACCEPT_CHARSET")();
    }+/
    ///リクエストuriを取得
    static string reqURI(){
      return Env.TGet!("REQUEST_URI")();
    }
    ///パスの情報
    static string pathInfo;
    ///スクリプトの絶対パス
    ///Bugs: ポートやhttps等に未対応
    static string path(){
      return "http://"~Env.TGet!("SERVER_NAME")()~getScriptName;
    }
    ///スクリプトの相対パス
    static string relPath;
    ///スクリプトのベースパス
    static string basePath;
    private static string _basePath(){
      string path=pathInfo;
      if(!path.length)
         return "";

      int len;
      findBeforesWithoutLast(path,'/',{len+=3;});
      scope dir=new char[len];
      for(int i;i<dir.length;i+=3)
        dir[i..i+3]="../";
      return assumeUnique(dir);
    }
    ///クッキー
    static string cookie;
    ///IP
    static string ip;
    ///UA
    static string useragent;
    ///ボットかどうか
    ///Safariが引っかかってるけど気にしない。
    static bool isBot;
    private static bool _isBot(){
      if(accept==""||accept=="*/*")
        return true;
      string ua=useragent.tolower();
      if(ua=="")
        return true;
/*      bool slash = false;
      for(int i = 0;i<ua.length;i++){
        switch(ua[i]){
          case '/':
            slash = true;
            break;
          case '@':
            return true;
          case 'b':
            if(startsWith(ua,"bot"))
              return true;
            break;
          case 'c':
            if(startsWith(ua,"crawler"))
              return true;
            break;
          case 'h':
            if(startsWith(ua,"http://"))
              return true;
            break;
          case 's':
            if(startsWith(ua,"slurp"))
              return true;
            if(startsWith(ua,"snoop"))
              return true;
            if(startsWith(ua,"spider"))
              return true;
            break;
          case 'w':
            if(startsWith(ua,"www."))
              return true;
            if(startsWith(ua,"winder"))
              return true;
            break;
          default:
        }
      }
      if(!slash)
        return true;*/
      if(ip[0..3]=="127") return false;//for develop
      if(ip[0..7]=="192.168") return false;//for home use
      string rip = std.string.join(std.string.split(ip,".").reverse,".");
      scope host = new InternetHost;
      if(host.getHostByName(rip~".niku.2ch.net"))//BBQ
        return true;
//      if(host.getHostByName(rip~".bbx.2ch.net"))//BBX
//        return true;
//      if(host.getHostByName(rip~".list.dsbl.org"))//DSDL (Dead)
//        return true;
      if(host.getHostByName(rip~".dnsbl.spam-champuru.livedoor.com"))//スパムちゃんぷるー
        return true;
      return false;
    }
    private static string accept;
    private static string acceptlanguage;
    ///ログ取り
    debug(HTTPLOG) static void writeLog(Log log,string flag){
      log.write(flag~":"~Env.get("REQUEST_METHOD")~"\t"~Env.get("SERVER_NAME")~getScriptName~Env.get("PATH_INFO")~"?"~Env.get("QUERY_STRING")~"\t"~Env.get("HTTP_USER_AGENT")~"\t"~Env.get("HTTP_ACCEPT")~"\t"~Env.get("HTTP_ACCEPT_LANGUAGE"));
    }
    ///クエリ
    static string[string] query;
    ///ファイル
    static File*[string] files;
    private this(){};
    //HTTPを扱うのを作成するコンストラクタ
    static this(){
      scriptname=Env.TGet!("SCRIPT_NAME")();
      if(scriptname.length==0)return null;
      ip=Env.TGet!("REMOTE_ADDR")();
      useragent=Env.TGet!("HTTP_USER_AGENT")();
      cookie=Env.TGet!("HTTP_COOKIE")();
      pathInfo=Env.TGet!("PATH_INFO")();//XXX: パスの正規化が必要かもしれない。
      bin=getAfter(getScriptName,'/');
      isnph=startWith(bin,"nph-");
      scope string querystr;
      switch(Env.TGet!("REQUEST_METHOD")()){
        case "GET":
          querystr=Env.TGet!("QUERY_STRING")();
          method=methods.GET;
          break;
        case "POST":
          scope rv = new ubyte[.toInt(Env.TGet!("CONTENT_LENGTH")())];
          method=methods.POST;
          StdIo.readStdIn(rv);
          querystr=cast(string)rv;
          break;
        case "HEAD":
          method=methods.HEAD;
          break;
        case "PUT":
          method=methods.PUT;
          break;
        case "DELETE":
          method=methods.DELETE;
          break;
        case "TRACE":
          method=methods.TRACE;
          break;
        default:
          method=methods.UNKNOWN;
      }
      accept=Env.TGet!("HTTP_ACCEPT")();
      acceptlanguage=Env.TGet!("HTTP_ACCEPT_LANGUAGE")();
      if(!startWith(strip(Env.TGet!("CONTENT_TYPE")()),"multipart/form-data")){
        getBefores(querystr,'&',delegate void(string i){
          int s=std.string.find(i,'=');
          if(s!=-1)Http.query[i[0..s]]=i[s+1..$];
        });
      }else{
        //XXX 未テスト。base64には対応していない。
        //filename='='の時問題あるかも
        uint start=std.string.find(Env.TGet!("CONTENT_TYPE")(),"boundary=")+"boundary=".length;
        string l="--"~Env.TGet!("CONTENT_TYPE")()[start..$];
        querystr=querystr[l.length+"\r\n".length..$-("--".length+l.length+"--".length)-"\r\n".length];
        string[] querylist=std.string.split(querystr,\r\n~l~\r\n);
        foreach(string i;querylist){
          string[] headers=std.string.split(getBefore(i,\r\n\r\n),\r\n);
          string[string] pref;
          foreach(string i2;headers){
            switch(getBefore(i2,':')){
              case "Content-Disposition":
                foreach(string i3;std.string.split(i2,";")){
                  string i4;
                  i4=strip(i3);
                  int t=std.string.find(i4,'=');
                  if(t==-1)continue;
                  pref[i4[0..t]]=i4[t+2..$-1];
                }
                break;
              default:
                break;
            }
          }
          if(auto name=("filename" in pref)){
            File* f=new File;
            f.name=*name;
            int s=std.string.find(i,\r\n\r\n);
            f.data=cast(ubyte[])i[s+4..$];
            files[pref["name"]]=f;
          }else{
            int s=std.string.find(i,\r\n\r\n);
            Http.query[pref["name"]]=i[s+4..$];
          }
        }
      }
      isBot = _isBot();
      basePath = _basePath();
      relPath = basePath~bin;
    }
  }
}

struct File{
  string name;
  ubyte[] data;
}

///HTTPのレスポンスなクラス
class HttpResponse{
  public{
    ///書き込み
    ///Bugs: gzipに未対応
    static void write(const(void)[] responseBody,uint status=200,string[string] headers=null){
      bool isNph=Http.isNph;
      scope string[] stream;
      if(isNph)stream~="HTTP/1.1 ";
      if(status!=200){
        if(!isNph)stream~="Status: ";
        if(status==304)
          stream~="304 Not Modified\r\n";
        else if(status==403)
          stream~="403 Forbidden\r\n";
        else if(status==404)
          stream~="404 File Not Found\r\n";
        else if(status==406)
          stream~="406 Not Acceptable\r\n";
        else if(status==301)
          stream~="301 Moved Permanently\r\n";
      }else if(isNph)stream~="200 OK\r\n";
      if(isNph)stream~="Connection: Keep-Alive\r\nKeep-Alive: timeout=15, max=100\r\n";
      stream~="Conten-Length: ";stream~=.toString(responseBody.length);stream~="\r\n";
      if(auto t="Vary" in headers)
        *t~=",Accept-Encoding";
      else
        stream~="Vary: Accept-Encoding\r\n";
      foreach(string key,inout string val;headers){
        stream~=key;stream~=": ";stream~=val;stream~="\r\n";
      }
      if(std.string.find(Env.TGet!("HTTP_ACCEPT_ENCODING")(),"deflate")!=-1){
        stream~="Content-Encoding: deflate\r\n";
        stream~="\r\n";
        compress(responseBody,(const(void)[] buf){stream~=cast(string)buf;});
      }else{
        stream~="\r\n";
        stream~=cast(string)responseBody;
      }
//      writefln(\n\n,stream);
      nazo.string.join((string str){StdIo.writeStdOut(str);},stream);
    }
  }
}
